/*
 *  Copyright 2011 derek.
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.aocore.util;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

/**
 *
 * @author the read method of this code was copied from D2Smells
 */
public class BitReader
{
    private static final char NULL_CHAR = '\0';

    private static final int BYTE_MASK = 0xFF;

    private static final int BYTE_BYTES = 1;
    private static final int WORD_BYTES = 2;
    private static final int DWORD_BYTES = 4;
    private static final int QWORD_BYTES = 8;

    private static final int BYTE_BITS = 8;
    private static final int WORD_BITS = 16;
    private static final int DWORD_BITS = 32;
    private static final int QWORD_BITS = 64;

    private boolean isBigEndian;
    private byte[] data;
    private int byteOffset;
    private int bitOffset;

    public BitReader(byte[] data, int byteOffset)
    {
        this.data = data;
        this.isBigEndian = false;
        this.byteOffset = byteOffset;
        this.bitOffset = 0;
    }

    public void skipBytes(int bytes)
    {
        this.byteOffset += bytes;
    }

    public void rewindBytes(int bytes)
    {
        this.byteOffset -= bytes;
    }

    public boolean getBoolean()
    {
        return read(BYTE_BITS) == 1 ? true : false;
    }

    public boolean getBoolean(int bits)
    {
        return read(bits) == 1 ? true : false;
    }

    public byte getByte()
    {
        return (byte) read(BYTE_BITS);
    }

    public byte getByte(int bits)
    {
        return (byte) read(bits);
    }

    public short getShort()
    {
        return (short) read(WORD_BITS);
    }

    public short getShort(int bits)
    {
        return (short) read(bits);
    }

    public int getInt()
    {
        return read(DWORD_BITS);
    }

    public int getInt(int bits)
    {
        return read(bits);
    }

    public String getString(int bits, char terminator, int maxLength)
    {
        StringBuilder stringBuilder = new StringBuilder();

        for (int i = 0; i < maxLength; i++)
        {
            char c = (char) getByte(bits);

            if (c == terminator)
            {
                break;
            }

            stringBuilder.append(c);
        }

        return stringBuilder.toString();
    }

    public String getString(int bits, char terminator)
    {
        return getString(bits, terminator, data.length - byteOffset);
    }

    public String getString(int bits)
    {
        return getString(bits, NULL_CHAR, data.length - byteOffset);
    }

    public String getString(char terminator)
    {
        return getString(BYTE_BITS, terminator, data.length - byteOffset);
    }

    public String getString()
    {
        return getString(BYTE_BITS, NULL_CHAR, data.length - byteOffset);
    }

    private int read(int bits)
    {
        ByteBuffer byteBuffer = ByteBuffer.allocate(4);
        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
        byte[] dest = byteBuffer.array();

        int cBits, destByte = 0, destBit = 0;

        if (this.isBigEndian)
        {
            destByte = bits >>> 3;
            destBit = bits & 7;
            if (destBit == 0) destByte--;
            else destBit = BYTE_BITS - destBit;

            while (true)
            {
                cBits = BYTE_BITS - (this.bitOffset > destBit ? this.bitOffset : destBit);
                if (bits < cBits) cBits = bits;

                if (cBits == BYTE_BITS)
                {
                    dest[destByte--] = this.data[this.byteOffset++];
                }
                else
                {
                    //NOTE: for reading and copying to individual vars, preserving low bits shouldn't be needed...
                    dest[destByte] = (byte)(
                        (dest[destByte] & ((BYTE_MASK << (BYTE_BITS - destBit)) | (BYTE_MASK >>> (destBit + cBits))))
                        | (((this.data[this.byteOffset] << this.bitOffset)
                            & (BYTE_MASK << (BYTE_BITS - cBits)) & BYTE_MASK) >>> destBit));

                    destBit += cBits;
                    if ((destBit & BYTE_BITS) == BYTE_BITS)
                    {
                        destBit ^= BYTE_BITS;
                        destByte--;
                    }
                    this.bitOffset += cBits;
                    if ((this.bitOffset & BYTE_BITS) == BYTE_BITS)
                    {
                        this.bitOffset ^= BYTE_BITS;
                        this.byteOffset++;
                    }
                }
                if ((bits -= cBits) == 0)
                    return byteBuffer.getInt();
            }
        }
        //else: Little Endian
        while (true)
        {
            cBits = BYTE_BITS - (this.bitOffset > destBit ? this.bitOffset : destBit);
            if (bits < cBits) cBits = bits;

            if (cBits == BYTE_BITS)
            {
                dest[destByte++] = this.data[this.byteOffset++];
            }
            else
            {
                //NOTE: for reading and copying to individual vars, preserving high bits shouldn't be needed...
                dest[destByte] = (byte)(
                    (dest[destByte] & ((BYTE_MASK >>> (BYTE_BITS - destBit)) | (BYTE_MASK << (destBit + cBits))))
                    | (((this.data[this.byteOffset] >>> this.bitOffset) & (BYTE_MASK >>> (BYTE_BITS - cBits)))
                        << destBit));

                destBit += cBits;
                if ((destBit & BYTE_BITS) == BYTE_BITS)
                {
                    destBit ^= BYTE_BITS;
                    destByte++;
                }
                this.bitOffset += cBits;
                if ((this.bitOffset & BYTE_BITS) == BYTE_BITS)
                {
                    this.bitOffset ^= BYTE_BITS;
                    this.byteOffset++;
                }
            }
            if ((bits -= cBits) == 0)
                return byteBuffer.getInt();
        }
    }
}
